Skip to main content
Version: 2.1.2

Smart calendar sync

To actually achieve two way syncing of calendar events between cloud provider and your service.

preparation

You need to

create database structures to store the calendar info

for databases, you need following structures:

App calendar

your app's calendar object type like:

type AppCalendar = {
id: string;
userId: string;
timezone?: string;
name?: string;
description?: string;
email?: string;
createdAt: string;
updatedAt: string;
}

this table is used for your app's display, daily use, etc, optional if you do not alredy have a table like this or you do not want one-to-many relations with your app calendar, you can skip creating this table.

Caldav calendar

you need to store caldav calendar information obtained from fetchCalendars

type CaldavCalendar = {
id: string;
userId: string;
timezone: string;
name: string;
source: string; // your caldav provider name
ctag: string; // obtained from remote
syncToken: string; // obtained from remote
url: string;
credentialId:
createAt: string;
};
credentials

save caldav calendar credentials in another table, encryption is recommended:

type CalendarCredential = {
account: string;
refreshToken?: string;
password?: string;
valid: boolean;
source: // your caldav provider name
}
caldav calendar objects

you also need to store caldav calendar objects obtained from fetchCalendarObjects

export type CalendarObject = {
id: string;
calendarId: string; // foreign key reference the CaldavCalendar if needed
url: string;
etag: string;
start: string; // recommend to have this field for easy filtering/sorting
end: string; // recommend to have this field for easy filtering/sorting
data: string; // actual ics data
};

to parse and obtain information from ics data, it's recommended to use a combination of

https://github.com/natelindev/pretty-jcal and https://github.com/kewisch/ical.js

for generating new ics data, it's recommended to use https://github.com/nwcell/ics.js/

Actual syncing

First you need to have user go through authorization process and obtain valid CalendarCredential, the method differs for each caldav provider. You need to find and setup it yourself.

after having obtained the credentials, you can begin the actual sync

First you need to get all stored calendars for the user

const localCalendars = await this.db.getCalendarByUserIdAndSource(
userId,
source
);

then you need to create caldav client using credentials

const client = new DAVClient({
serverUrl: 'https://caldav.icloud.com',
credentials: {
username: 'YOUR_APPLE_ID',
password: 'YOUR_APP_SPECIFIC_PASSWORD',
},
authMethod: 'Basic',
defaultAccountType: 'caldav',
});
Remote to local

you can use syncCalendars function from the lib with detailedResult set as true

const { created, updated, deleted } = await client.syncCalendars({
oldCalendars: localCalendars.map((lc) => ({
displayName: lc.name,
syncToken: lc.syncToken,
ctag: lc.ctag,
url: lc.url,
})
),
detailedResult: true,
});

make sure you send the syncToken and ctag, this way the remote will know your last sync and identify the calendar changes.

now you have all calendar changes on remote. make actual changes to your database like:

await this.db.transaction(async (tx) => {

await Promise.all(created.map(async (c) => {
// created
const calendarObjects = await client.fetchCalendarObjects({ calendar: c })

if (calendarObjects.length > 0) {
await Promise.all(
calendarObjects.map((co) => {
// parse start end time if needed
// const parsedObject = parse(co);
// const { start, end } = parsedObject;
return this.db.createCalendarObject(tx, {
...co,
// start,
// end,
calendarId: c.id
})
})
)
}
}))

// deleted
if (deleted.length > 0) {
await this.db.deleteByUrls(
tx,
filteredDeleted.map((d) => d.url)
);
}

// updated
const localCalendarsToBeUpdated = await this.db.getByUrls(
tx,
updated.map((u) => u.url)
);

// find out and apply the change on calendar
await Promise.all(
localCalendarsToBeUpdated.map(async (lc) => {
const localObjects = await this.db.getCalendarById(
tx,
lc.id
);
const {
created: createdObjects,
updated: updatedObjects,
deleted: deletedObjects,
} = (
await client.smartCollectionSync({
collection: {
url: lc.url,
ctag: lc.ctag,
syncToken: lc.syncToken,
objects: localObjects,
objectMultiGet: client.calendarMultiGet,
},
method: 'webdav',
detailedResult: true,
})
).objects;

// apply changes to local calendar objects
// created objects
if (createdObjects.length > 0) {
await Promise.all(
createdObjects
.filter((co) => co.url.includes('.ics'))
.map((co) => {
// parse start end time if needed
// const parsedObject = parse(co);
// const { start, end } = parsedObject;

return this.db.createCalendarObject(tx, {
...co,
// start,
// end,
url: URL.resolve(lc.url, co.url),
calendarId: lc.id,
});
})
);
}

// deleted objects
if (deletedObjects.length > 0) {
await this.db.deleteCalendarObjectByUrls(
tx,
deletedObjects.map((d) => URL.resolve(lc.url, d.url))
);
}

// updated objects
if (updatedObjects.length > 0) {
await Promise.all(
updatedObjects.map((uo) => {
// parse start end time if needed
// const parsedObject = parse(co);
// const { start, end } = parsedObject;

return this.db.updateCalendarObjectByUrl(
tx,
URL.resolve(lc.url, uo.url),
{
etag: uo.etag,
data: uo.data,
start,
end,
}
);
})
);
}
})
);

// update the syncToken & ctag for the calendars to be updated
await Promise.all(
filteredUpdated.map((u) => {
const lcu = localCalendarToBeUpdated.find((uo) => uo.url === u.url);
if (!lcu) {
throw new ValidationError(`local calendar with url ${u.url} not found `);
}
return this.db.updateCalendarById(tx, lcu.id, {
syncToken: u.syncToken,
ctag: u.ctag,
});
})
);

})
Local to remote

when going local to remote, it's rather easy

just generate the ics data and then use createCalendarObject , updateCalendarObject and deleteCalendarObject directly on remote caldav calendars. Update your locally stored calendar objects in your database after remote operation success.